梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)
最小生成树(Minimum Spanning Tree,MST)是无向连通带权图的核心概念,指在包含图中所有顶点的前提下,选取若干条边构成一棵树(无环),且所有边的权值之和最小。它广泛应用于通信网络搭建、道路规划、电路设计等场景,核心解决 “以最小成本连接所有节点” 的问题。本文将详细讲解最小生成树的原理与实现,内容由浅入深,所有代码可直接编译运行。
图结构的实现方式:
学习经典应用场景前,请根据上面的教程封装好自定义图,所有场景实例直接复用
一个有N个顶点的图,边一定是大于等于N-1的。图的最小生成树,就是在这些边中选择N-1条出来,连接所有的N个点。这N-1条边的边权之和是所有方案中最小的。
最小生成树仅适用于无向连通带权图:无向保证边的双向连通性,连通保证存在生成树,带权是求 “最小” 的前提;若图不连通,仅能求各连通分量的最小生成森林。
求最小生成树的核心是贪心策略(每一步选择局部最优,最终得到全局最优),最常用的两个算法各有侧重,适配不同场景:
| 算法 | 核心思想 | 时间复杂度 | 适配场景 |
|---|---|---|---|
| Prim(普里姆) | 从单个顶点出发,逐步扩展顶点 | O(n2)(邻接矩阵)/O(Mlogn)(邻接表 + 堆) | 稠密图(顶点少、边多) |
| Kruskal(克鲁斯卡尔) | 按边权升序选边,避免形成环 | O(MlogM)(主要为边排序耗时) | 稀疏图(顶点多、边少) |
核心差异:Prim 是按顶点扩展,Kruskal 是按边筛选;Kruskal 依赖前文的并查集(判断选边是否形成环),是更易实现且工程中更常用的版本。
按边的权值从小到大依次选择,若选择的边的两个端点不属于同一个连通分量(即选此边不会形成环),则将其加入最小生成树;若形成环则跳过,直到选够n-1 条边(n 为顶点数),此时所有顶点已连通,得到最小生成树。
算法分析:
假设无向连通带权图有n个顶点、m条边:
#include <iostream>
#include <vector>
#include"ArrayGraph.h"
using namespace std;
// 边的结构体:存储无向边的两个顶点和权值
struct Edge {
int u, v, weight; // u-起点,v-终点,weight-边权
// 重载 < 运算符:用于sort按边权从小到大排序
bool operator < (const Edge& other) const {
return weight < other.weight;
}
};
vector<int> parent; // 父节点数组
vector<int> ranks; // 秩数组(树的高度)
// 查找:带路径压缩
int find(int x) {
// 先找到根节点
int root = x;
while (parent[root] != root) {
root = parent[root];
}
// 路径压缩:将查找路径上所有节点直接指向根节点
while (parent[x] != root) {
int next = parent[x]; // 保存x的原父节点
parent[x] = root; // x直接指向根节点
x = next; // 继续处理原父节点
}
return root;
}
// 合并:按秩合并
bool unite(int x, int y) {
x = find(x); // 找到x的根
y = find(y); // 找到y的根
if (x == y) return false; // 同一集合,直接返回
// 按秩合并:小秩树指向大秩树
if (ranks[x] < ranks[y]) {
parent[x] = y;
} else {
parent[y] = x;
// 秩相等时,合并后根节点秩+1
if (ranks[x] == ranks[y]) {
ranks[x]++;
}
}
return true;
}
// Kruskal算法主函数
// 返回值:最小生成树的总权值;若图不连通,返回-1
int kruskal(ArrayGraph& graph, vector<Edge>& edges) {
int n = graph.vertexNum;
// 步骤1:按边权从小到大排序所有边(贪心核心)
sort(edges.begin(), edges.end());
int totalWeight = 0; // 最小生成树的总权值
int edgeCount = 0; // 已加入最小生成树的边数
// 步骤2:依次遍历排序后的边,尝试加入最小生成树
for (const Edge& e : edges) {
// 若两个顶点不连通,合并并加入边
if (unite(e.u, e.v)) {
totalWeight += e.weight;
edgeCount++;
cout << "选择边:(" << graph.vertices[e.u] << ", " << graph.vertices[e.v] << "),权值:" << e.weight << endl;
// 最小生成树需n-1条边,提前终止
if (edgeCount == n - 1) {
break;
}
}
}
// 若边数不足n-1,说明图不连通,无最小生成树
return (edgeCount == n - 1) ? totalWeight : -1;
}
// 测试案例
int main() {
ArrayGraph graph;
vector<Edge> edges;
// 1. 添加顶点(A、B、C、D、E、F、G、H、I)
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
// 2. 添加边(有权无向图)
graph.addEdge('A','C',2);
graph.addEdge('A','D',4);
graph.addEdge('A','E',7);
graph.addEdge('B','C',2);
graph.addEdge('B','D',6);
graph.addEdge('C','A',2);
graph.addEdge('C','D',1);
graph.addEdge('C','B',2);
graph.addEdge('D','A',4);
graph.addEdge('D','B',6);
graph.addEdge('D','C',1);
graph.addEdge('D','E',1);
graph.addEdge('E','A',7);
graph.addEdge('E','D',1);
// 3. 打印邻接矩阵
graph.printAdjacency();
// 4. 初始化并查集
parent.resize(graph.vertexNum);
ranks.resize(graph.vertexNum, 1); // 初始秩均为1
for (int i = 0; i < graph.vertexNum; i++) {
parent[i] = i; // 初始父节点为自身,自成集合
}
// 5. 生成边集合
for(int i=0;i < graph.vertexNum;i++)
{
for(int j=0;j < graph.vertexNum;j++)
{
if(graph.graph[i][j]>NO_EDGE)
{
edges.push_back({i,j,graph.graph[i][j]});
}
}
}
// 6. 最小生成树
int minTotal = kruskal(graph, edges);
if (minTotal == -1) {
cout << "图不连通,无最小生成树!" << endl;
} else {
cout << "最小生成树的总权值:" << minTotal << endl;
}
return 0;
}
===== 带权邻接矩阵 =====
A B C D E
A 0 0 2 4 7
B 0 0 2 6 0
C 2 2 0 1 0
D 4 6 1 0 1
E 7 0 0 1 0
========================
Kruskal算法求解最小生成树过程:
选择边:(C, D),权值:1
选择边:(D, E),权值:1
选择边:(A, C),权值:2
选择边:(B, C),权值:2
最小生成树的总权值:6
将图的顶点集划分为两个部分:已选集合 U:当前已纳入最小生成树的顶点(初始时可任选一个顶点,通常选顶点 0);未选集合 V-U:尚未纳入最小生成树的顶点。
算法的每一步贪心选择:找到连接U 和 V-U的所有边中权值最小的那条边,将该边对应的未选顶点加入 U 集合。重复此过程,直到 U 包含图中所有顶点,此时所有选中的边即构成最小生成树。
算法分析:
设图有 n 个顶点,邻接矩阵为graph[n][n](graph[i][j]表示顶点 i 到 j 的边权,无边时设为无穷大INF,自身到自身为 0),步骤如下:
#include <iostream>
#include"ArrayGraph.h"
using namespace std;
// Prim算法:邻接矩阵版
void prim(ArrayGraph& graph) {
int n = graph.vertexNum;
int lowcost[MAX_VERTEX]; // 未选顶点到已选集合U的最小权值,0表示已在U中
int adjvex[MAX_VERTEX]; // 未选顶点的最小权值边对应的U中顶点
int total = 0; // 最小生成树的总权值
// 初始化,无边时设为无穷大INF,自身到自身为 0
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
if(i == j)
{
//将自身至自身的距离设置为0
graph.graph[i][j] = 0;
}
else if(graph.graph[i][j] == NO_EDGE)
{
graph.graph[i][j] = INF;//把不相连的点之间的距离设为无穷大(INF)
}
}
}
// 步骤1:初始化辅助数组,从顶点0开始(加入U)
for (int i = 0; i < n; i++) {
lowcost[i] = graph.graph[0][i]; // 初始时所有未选顶点的最小边指向顶点0
adjvex[i] = 0; // 对应U中的顶点为0
}
lowcost[0] = 0; // 顶点0加入U,标记为0
// 步骤2:迭代n-1次,选择n-1条边(生成树需n-1条边)
for (int i = 1; i < n; i++) {
// 2.1 找到未选顶点中lowcost最小的顶点k
int min_val = INF;
int k = -1;
for (int j = 0; j < n; j++) {
if (lowcost[j] != 0 && lowcost[j] < min_val) {
min_val = lowcost[j];
k = j;
}
}
// 若k=-1,说明图不连通,无最小生成树(题目保证连通时可省略)
if (k == -1) {
cout << "图不连通,无最小生成树!" << endl;
return;
}
// 2.2 输出本次选择的最小边(adjvex[k], k)及其权值
cout << "选择边:(" << graph.vertices[adjvex[k]] << ", " << graph.vertices[k] << "),权值:" << min_val << endl;
total += min_val; // 累加总权值
// 2.3 将顶点k加入U,更新辅助数组
lowcost[k] = 0; // 标记k为已选
for (int j = 0; j < n; j++) {
// 若j未选,且k到j的边权 < j当前到U的最小权值,则更新
if (lowcost[j] != 0 && graph.graph[k][j] < lowcost[j]) {
lowcost[j] = graph.graph[k][j];
adjvex[j] = k;
}
}
}
// 输出最小生成树的总权值
cout << "最小生成树的总权值:" << total << endl;
}
// 测试案例
int main() {
ArrayGraph graph;
// 1. 添加顶点(A、B、C、D、E)
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
// 2. 添加边(有权无向图)
graph.addEdge('A','C',2);
graph.addEdge('A','D',4);
graph.addEdge('A','E',7);
graph.addEdge('B','C',2);
graph.addEdge('B','D',6);
graph.addEdge('C','A',2);
graph.addEdge('C','D',1);
graph.addEdge('C','B',2);
graph.addEdge('D','A',4);
graph.addEdge('D','B',6);
graph.addEdge('D','C',1);
graph.addEdge('D','E',1);
graph.addEdge('E','A',7);
graph.addEdge('E','D',1);
// 3. 打印邻接矩阵
graph.printAdjacency();
// 4. 最小生成树
cout << "Prim算法求解最小生成树过程:" << endl;
prim(graph);
return 0;
}
===== 带权邻接矩阵 =====
A B C D E
A 0 0 2 4 7
B 0 0 2 6 0
C 2 2 0 1 0
D 4 6 1 0 1
E 7 0 0 1 0
========================
Prim算法求解最小生成树过程:
选择边:(A, C),权值:2
选择边:(C, D),权值:1
选择边:(D, E),权值:1
选择边:(C, B),权值:2
最小生成树的总权值:6
Prim 算法:首选「稠密图」
Kruskal 算法:首选「稀疏图」